package edu.unice.simul.modele.moteur;

import java.util.Arrays;
import java.util.List;

import edu.unice.simul.conf.GeneralSettings;
import edu.unice.simul.controleur.SimulationControleur;
import edu.unice.simul.modele.Piece;
import edu.unice.simul.modele.contenus.Contenu;
import edu.unice.simul.modele.contenus.immobiles.Mur;
import edu.unice.simul.modele.contenus.immobiles.Sortie;
import edu.unice.simul.modele.contenus.mobiles.Humain;
import edu.unice.simul.util.Block;
import edu.unice.simul.util.Dim;
import edu.unice.simul.util.DimDouble;
import edu.unice.simul.util.graphs.Dijkstra;
import edu.unice.simul.util.graphs.Graphe;
import edu.unice.simul.util.graphs.Sommet;
import edu.unice.simul.vue.initialisation.modes_dessin.BlockType;

/**
 * Moteur dynamique
 */
public class Moteur {
	/**
	 * Évolution du temps
	 */
	public double deltaT;
	/**
	 * Temps total
	 */
	private double temps = 0;
	/**
	 * L'évacuation est-elle lancée ?
	 */
	private boolean lancerEvacuation;
	private Calcul calcul;
	private Piece piece;
	/**
	 * La simulation est-elle finie ?
	 */
	private boolean fini = true;
	private SimulationControleur controleur = null;
	private Sommet[][] sommets = null;

	public Moteur(boolean lancerEvacuation) {
		this(lancerEvacuation, 0.1);
	}

	/**
	 * @param dt
	 *          pas de temps
	 */
	public Moteur(boolean lancerEvacuation, double dt) {
		if (dt < 0 || dt > 10)
			throw new IllegalArgumentException(
					"La valeur de dt ne doit pas être négative.");

		this.lancerEvacuation = lancerEvacuation;
		deltaT = dt;
		this.calcul = new Calcul();
	}

	public void setControleur(SimulationControleur controleur) {
		this.controleur = controleur;
	}

	/**
	 * Renvoie un tableau contenant la pondération de chacun des block de la pièce
	 */
	public Double[][] getSommetsDistance() {
		Double[][] arrayRetour = new Double[sommets.length][sommets[0].length];
		for (int i = 0; i < sommets.length; i++)
			for (int j = 0; j < sommets[i].length; j++)
				arrayRetour[i][j] = sommets[i][j].getDistance();

		return arrayRetour;
	}

	/**
	 * Deplace les humains suivant La direction de la sortie la plus proche Une
	 * vitesse constante
	 * 
	 */
	public List<Humain> pasSuivant(Piece piece) {
		this.piece = piece;
		List<Humain> humains;
		humains = piece.getHumains();
		fini = true;

		AjusteVitesseSouhaitee ajusteVitesseSouhaitee = new AjusteVitesseSouhaitee();

		if (!lancerEvacuation) {
			fini = false;

			for (Humain h : humains) {
				if (h.enVie()) {
					h.ballade();
					ajusteVitesseSouhaitee.calcul(h);
					calculer(h);
				}
			}
		} else {
			if (sommets == null) {
				long t = System.currentTimeMillis();
				sommets = construireGraphe();
				pondererSommets();
				if (GeneralSettings.DEBUG)
					System.err.println("Temps de clacul du graphe : "
							+ (System.currentTimeMillis() - t) + " milisecondes");
				if (controleur != null)
					controleur.stopLoading();
			}
			for (Humain h : humains) {
				if (!h.estSorti() && h.enVie()) {
					fini = false;
					h.setDirectionSouhaitee(getDirection(h));
					ajusteVitesseSouhaitee.calcul(h);
					calculer(h);

					List<Contenu> list = piece.getContenuAt(h.getPosition());
					for (Contenu c : list)
						if (c instanceof Sortie) {
							h.setSorti(temps, c.getPosition());
							break;
						}
				}
			}
		}
		this.temps += deltaT;

		return humains;
	}

	/**
	 * Calcul des nouvelles coordonees En utilisant la relation de la dynamique et
	 * une force faisant intervenir la vitesse souhaitee.
	 * 
	 */
	public void calculer(final Humain h) {
		calcul.calculer(h);
	}

	/**
	 * Lance l'évacuation de la pièce
	 */
	public void lancerEvacuation() {
		this.lancerEvacuation = true;
	}

	/**
	 * Construit le graphe correspondant à la pièce
	 */
	private Sommet[][] construireGraphe() {
		Sommet[][] sommets = new Sommet[piece.getDimmensions().getX()][piece
				.getDimmensions().getY()];

		// Création des sommets
		for (int i = 0; i < piece.getDimmensions().getY(); i++) {
			for (int j = 0; j < piece.getDimmensions().getX(); j++) {
				List<Contenu> c = piece.getContenuAt(new Dim(j, i));
				Block block = null;

				if (c.isEmpty())
					block = new Block(new Dim(j, i), BlockType.NOTHING);
				else {
					Contenu contenu = c.get(0);

					if (contenu instanceof Mur)
						block = new Block(contenu.getPosition(), BlockType.MUR);
					else if (contenu instanceof Humain)
						block = new Block(contenu.getPosition(), BlockType.HUMAIN);
					else
						block = new Block(contenu.getPosition(), BlockType.SORTIE);
				}
				sommets[j][i] = new Sommet(block);
			}
		}

		// Création des arcs
		for (int i = 0; i < piece.getDimmensions().getY(); i++) {
			for (int j = 0; j < piece.getDimmensions().getX(); j++) {
				Sommet s = sommets[j][i];

				if (s.getBlock().getType() != BlockType.MUR) {

					Sommet left, right, top, bottom;

					if (i != 0)
						left = sommets[j][i - 1];
					else
						left = null;

					if (i < piece.getDimmensions().getY() - 1)
						right = sommets[j][i + 1];
					else
						right = null;

					if (j != 0)
						top = sommets[j - 1][i];
					else
						top = null;

					if (j < piece.getDimmensions().getX() - 1)
						bottom = sommets[j + 1][i];
					else
						bottom = null;

					if (left != null && left.getBlock().getType() != BlockType.MUR)
						s.addArc(left, 1);

					if (top != null && top.getBlock().getType() != BlockType.MUR)
						s.addArc(top, 1);

					if (right != null && right.getBlock().getType() != BlockType.MUR)
						s.addArc(right, 1);

					if (bottom != null && bottom.getBlock().getType() != BlockType.MUR)
						s.addArc(bottom, 1);

					// Top left
					if (i != 0 && j != 0 && left != null && top != null
							&& left.getBlock().getType() != BlockType.MUR
							&& top.getBlock().getType() != BlockType.MUR) {
						Sommet topleft = sommets[j - 1][i - 1];
						if (topleft.getBlock().getType() != BlockType.MUR)
							s.addArc(topleft, Math.sqrt(2));
					}

					// Top right
					if (i < piece.getDimmensions().getY() - 1 && j != 0 && right != null
							&& top != null && right.getBlock().getType() != BlockType.MUR
							&& top.getBlock().getType() != BlockType.MUR) {
						Sommet topright = sommets[j - 1][i + 1];
						if (topright.getBlock().getType() != BlockType.MUR)
							s.addArc(topright, Math.sqrt(2));
					}

					// Bottom left
					if (i != 0 && j < piece.getDimmensions().getX() - 1 && left != null
							&& bottom != null && left.getBlock().getType() != BlockType.MUR
							&& bottom.getBlock().getType() != BlockType.MUR) {
						Sommet bottomleft = sommets[j + 1][i - 1];
						if (bottomleft.getBlock().getType() != BlockType.MUR)
							s.addArc(bottomleft, Math.sqrt(2));
					}

					// Bottom right
					if (i < piece.getDimmensions().getY() - 1
							&& j < piece.getDimmensions().getX() - 1 && bottom != null
							&& right != null && right.getBlock().getType() != BlockType.MUR
							&& bottom.getBlock().getType() != BlockType.MUR) {
						Sommet bottomright = sommets[j + 1][i + 1];
						if (bottomright.getBlock().getType() != BlockType.MUR)
							s.addArc(bottomright, Math.sqrt(2));
					}
				}
			}
		}

		return sommets;
	}

	/**
	 * Pondère le graphe. Dijkstra est éxécuté à partir de chaque sortie et
	 * pondère les sommets tel que chaque sortie a comme valeur 0 et plus le
	 * chemin vers celles-ci est grand, plus la pondération des sommets augmente.
	 * 
	 * Un arc liant deux sommets sur le même axe coûte 1, un axe reliant deux
	 * sommets en diagonale coûte racine de 2.
	 */
	private void pondererSommets() {
		if (sommets == null)
			return;

		for (int i = 0; i < piece.getDimmensions().getY(); i++) {
			for (int j = 0; j < piece.getDimmensions().getX(); j++) {
				Sommet s = sommets[j][i];
				if (s.getBlock().getType() == BlockType.SORTIE) {
					Graphe myGraph = new Graphe(sommets[j][i]);

					Dijkstra dAlg = new Dijkstra(myGraph);
					dAlg.calculer();
				}
			}
		}

		if (GeneralSettings.DEBUG)
			System.err.println(Arrays.deepToString(sommets));
	}

	/**
	 * Retourne la direction de l'humain (en radians) pour aller vers le sommet le
	 * moins cher dans les cases adjascentes.
	 */
	private double getDirection(Humain h) {
		int x = h.getPosition().getX();
		int y = h.getPosition().getY();

		double val = sommets[x][y].getDistance();
		double bestX = x;
		double bestY = y;

		Sommet left = null, right = null, top = null, bottom = null;

		if (x != 0)
			left = sommets[x - 1][y];

		if (x < piece.getDimmensions().getX() - 1)
			right = sommets[x + 1][y];

		if (y != 0)
			top = sommets[x][y - 1];

		if (y < piece.getDimmensions().getY() - 1)
			bottom = sommets[x][y + 1];

		Sommet topleft = null, topright = null, bottomleft = null, bottomright = null;

		// Top left
		if (y != 0 && x != 0 && left != null && top != null
				&& left.getBlock().getType() != BlockType.MUR
				&& top.getBlock().getType() != BlockType.MUR) {
			topleft = sommets[x - 1][y - 1];
		}

		// Top right
		if (y < piece.getDimmensions().getY() - 1 && x != 0 && right != null
				&& top != null && top.getBlock().getType() != BlockType.MUR
				&& right.getBlock().getType() != BlockType.MUR) {
			topright = sommets[x - 1][y + 1];
		}

		// Bottom left
		if (y != 0 && x < piece.getDimmensions().getX() - 1 && bottom != null
				&& left != null && left.getBlock().getType() != BlockType.MUR
				&& bottom.getBlock().getType() != BlockType.MUR) {
			bottomleft = sommets[x + 1][y - 1];
		}

		// Bottom right
		if (y < piece.getDimmensions().getY() - 1
				&& x < piece.getDimmensions().getX() - 1 && bottom != null
				&& right != null && bottom.getBlock().getType() != BlockType.MUR
				&& right.getBlock().getType() != BlockType.MUR) {
			bottomright = sommets[x + 1][y + 1];
		}

		Sommet[] _sommets = { left, top, right, bottom, bottomleft, bottomright,
				topleft, topright };

		for (Sommet s : _sommets)
			if (s != null) {
				if (s.getDistance() < val) {
					val = s.getDistance();
					bestX = s.getBlock().getPos().getX();
					bestY = s.getBlock().getPos().getY();
				}
			}

		// Si l'humain ne peut pas sortir, il meurt
		if (bestX == x && bestY == y)
			h.diminuerVie(100.);

		return directionFromPos(new DimDouble(h.getAbscisse(), h.getOrdonnee()),
				new DimDouble(bestX + .5, bestY + .5));
	}

	/**
	 * Calcule la direction à prendre pour un humain pour aller d'une position à
	 * une autre.
	 * 
	 * @param origine
	 *          coordonnées d'origine
	 * @param destination
	 *          coordonnées de la destination
	 */
	private double directionFromPos(DimDouble origine, DimDouble destination) {
		double teta = Math.PI / 2; // dans le cas ou x = 0

		double x = destination.getX() - origine.getX();
		double y = destination.getY() - origine.getY();

		if (x == 0) {
			if (y < 0)
				teta = -teta; // dans le cas ou x = 0
		} else
			teta = Math.atan2(y, x);
		
		return teta;
	}

	/**
	 * Renvoie le temps passé pendant la simulation
	 */
	public long getTime() {
		return (long) temps * 1000;
	}

	/**
	 * La simulation est-elle finie ?
	 */
	public boolean estFini() {
		return this.fini;
	}
}